/*
 * SubRoute.java
 * 
 * Copyright 2008 David D. Emory
 * 
 * This file is part of Five Points. See <http://www.fpdev.org> for
 * additional information regarding the project.
 * 
 * Five Points is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.---
 * 
 * Five Points is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Five Points.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.fpdev.core.transit;

import au.com.bytecode.opencsv.CSVWriter;
import org.fpdev.core.routenet.RouteNetwork;
import org.fpdev.core.routenet.RLinkSub;
import java.awt.Color;
import java.awt.geom.Point2D;
import java.io.File;
import java.io.FileWriter;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.fpdev.core.FPEngine;
import org.fpdev.core.basenet.BLink;
import org.fpdev.core.basenet.BNode;
import org.fpdev.apps.rtemaster.gui.map.MapCanvas;
import org.fpdev.util.FPUtil;

import javax.swing.table.*;
import org.fpdev.core.basenet.BaseNetwork;
import org.fpdev.core.basenet.Path;

public class SubRoute implements Comparable {

  private String id_,  name_,  rteSuffix_;
  private Route containingRoute_;
  private TransitPath path_;
  private Map<String, Timetable> timetables_;
  //private Map<Timetable.DayOfWeek, Timetable> timetables_;
  private int[] tptIndices_;
  private List<Integer> linkIDs_;
  private List<RLinkSub> routeLinks_;


  public SubRoute(Route route, String id, FPEngine engine, TransitPath.Type pathType) {
    containingRoute_ = route;
    id_ = id;
    name_ = rteSuffix_ = "";
    path_ = new TransitPath(engine, this, pathType);
    linkIDs_ = new LinkedList<Integer>();
    timetables_ = new TreeMap<String, Timetable>();
    //timetables_ = new TreeMap<Timetable.DayOfWeek, Timetable>();
    routeLinks_ = new LinkedList<RLinkSub>();
  }

  public SubRoute(SubRoute sub, FPEngine engine) {
    containingRoute_ = sub.containingRoute_;
    id_ = sub.id_;
    name_ = sub.name_;
    rteSuffix_ = sub.rteSuffix_;
    path_ = sub.path_.createCopy(engine, this, false); //new TransitPath(engine, this, pathType);
    linkIDs_ = new LinkedList<Integer>();
    linkIDs_.addAll(sub.linkIDs_);
    timetables_ = new TreeMap<String, Timetable>();
    timetables_.putAll(sub.timetables_);
    routeLinks_ = new LinkedList<RLinkSub>();
    routeLinks_.addAll(sub.routeLinks_);
  }

  public SubRoute(Route route, FPEngine engine, SerializableSubRoute ssub) {
    this(route, ssub.getID(), engine, ssub.getPathType());
    //ssub.initStops(path_, engine);
    timetables_ = ssub.getTimeTableMap();
    name_ = ssub.getName();
    rteSuffix_ = ssub.getRouteSuffix();

    Iterator<Integer> linkIDs = ssub.getLinkIDs();
    while (linkIDs.hasNext()) {
      this.addLinkID(linkIDs.next());
    }
    Iterator<SerializableStop> stops = ssub.getStops();
    while (stops.hasNext()) {
      path_.initStop(stops.next().createStop(path_, engine.getBaseNet()));
    }

    initTimePointIndices();
  }

  public void delete() {
    // unregister link-path associations
    path_.clear();
  }

  public boolean setID(String id) {
    if (id.length() == 0 || !isValidDirection(getDirPrefix(id))) {
      return false;
    }

    String oldID = id_;
    id_ = id;
    containingRoute_.subRouteIDChanged(this, oldID);

    return true;
  }

  /**
   * Accessor for the subroute's short alphanumeric identifier. Does not include
   * any containing route or provider information.
   * 
   * <p>The standard format for subroute IDs is "DIR[_DESC]", where:
   * <ul>
   * <li>DIR is one of the recognized direction codes (NB, SB, EB, WB, IB, OB,
   * CW, CCW, or RT)
   * <li>DESC (optional; separated by '_' when specified) is an additional short
   * descriptor (e.g. "AM" for morning routing); typically used when there are
   * multiple subroutes corresponding to a single direction.
   * </ul> 
   * 
   * @return this subroute's basic ID
   */
  public String getID() {
    return id_;
  }

  /**
   * Accessor for the "full" ID, a combination of the containing route's ID (not
   * including provider information) and the subroute ID.
   * 
   * @return this subroute's full ID
   */
  public String getFullID() {
    return containingRoute_.getID() + ":" + id_;
  }

  /** 
   * Accessor for the "master" ID, which includes the containing provider ID,
   * containing route ID, and the subroute ID. The most comprehensive identifier
   * for a subroute.
   * 
   * @return String representing this subroute's master ID
   */
  public String getMasterID() {
    return containingRoute_.getCombinedID() + ":" + id_;
  }

  /**
   * Isolates and returns the direction code portion of the subroute's ID.
   * 
   * @return  string value of the direction code
   */
  public String getDirCode() {
    return getDirPrefix(id_);
  }

  /**
   * Static version of getDirCode().
   * 
   * @param id  the full subroute ID 
   * @return  string value of the direction code
   */
  public static String getDirPrefix(String id) {
    return id.split("_")[0];
  }

  /** 
   * Tests validity of a subroute ID direction code. See wiki docs for current
   * listing of valid codes.
   * 
   * @param dir the direction code to test
   * @return boolean result of test 
   */
  public static boolean isValidDirection(String dir) {
    return dir.equals("NB") ||
            dir.equals("SB") ||
            dir.equals("EB") ||
            dir.equals("WB") ||
            dir.equals("IB") ||
            dir.equals("OB") ||
            dir.equals("CW") ||
            dir.equals("CCW") ||
            dir.equals("RT");
  }

  /**
   * Accessor for the descriptive name of the subroute. Automatically converts
   * common abbreviations ("am", "pm", "peak", etc.) to the fully spelled out
   * name.
   * 
   * @return This subroute's name
   */
  public String getName() {
    if (name_.length() > 0) {
      return name_;
    }
    String[] splitID = id_.split("_", 2);
    if (splitID.length >= 2 && splitID[1].length() > 0) {
      String restID = splitID[1].toLowerCase();
      if (restID.equals("pm")) {
        return "PM Routing";
      }
      if (restID.equals("am")) {
        return "AM Routing";
      }
      if (restID.equals("lnt")) {
        return "Late Night Truncated";
      }
      if (restID.equals("emt")) {
        return "Early Morning Truncated";
      }
      if (restID.equals("sat")) {
        return "Saturday Routing";
      }
      if (restID.equals("sun")) {
        return "Sunday Routing";
      }
      if (restID.equals("ss")) {
        return "Weekend Routing";
      }
      if (restID.equals("peak")) {
        return "Peak Hour Routing";
      }
      return splitID[1] + " trips";
    }
    return "Standard";
  }

  /**
   * Mutator for subroute descriptive name field.
   * 
   * @param name new subroute name
   */
  public void setName(String name) {
    name_ = name;
  }

  /**
   * Accessor for the route suffix, a short code (often only one character) 
   * appended to the parent route's ID for display purposes. For example, if bus
   * route "23" has a special express pattern branded as "23X", the route suffix 
   * for the corresponding subroute would be "X".
   *   
   * @return the suffix string
   */
  public String getRouteSuffix() {
    return rteSuffix_;
  }

  /**
   * Mutator for the route suffix. (See getRouteSuffix() for description.)
   * 
   * @param rteSuffix the suffix string
   */
  public void setRouteSuffix(String rteSuffix) {
    rteSuffix_ = rteSuffix;
  }

  /**
   * Accessor for the route that contains this subroute
   * 
   * @return Route the containing route
   */
  public Route getRoute() {
    return containingRoute_;
  }

  /**
   * Accessor for this subroute's TransitPath.
   * 
   * @return TransitPath object
   */
  public TransitPath getPath() {
    return path_;
  }

  /**
   * Sets/replaces the TransitPath associated with this subroute. Reinitializes
   * the internal timepoint indices table (if applicable).
   * 
   * @param path the new TransitPath
   */
  public void setPath(TransitPath path) {
    System.out.println("path=" + path_);
    if (path_ != null) {
      path_.unregisterAllLinks();
    }
    path_ = path;
    path_.registerAllLinks();
    initTimePointIndices();
  }

  /**
   * Counts the number of day-of-week timetables defined for this subroute.
   * 
   * @return integer count value
   */
  public int timeTableCount() {
    return timetables_.size();
  }

  /**
   * Checks if a specific day-of-week timetable has been defined for this
   * subroute.
   * 
   * @param day the day-of-week code
   * @return true if table exists for specified day
   */
  public boolean hasTable(CalendarService service) {
    return timetables_.containsKey(service.getID());
  }

  /**
   * Gets a timetable for the specified day-of-week code.
   * 
   * @param day the day-of-week code
   * @return requested timetable, or null if no table defined for DOW
   */
  public Timetable getTable(CalendarService service) {
    return timetables_.get(service.getID());
  }

  /**
   * Gets collection of timetables for all days for which one is defined.
   * 
   * @return iterator of Timetable objects
   */
  public Collection<Timetable> getTables() {
    return timetables_.values();
  }

  /**
   * Clears all timetables for this subroute.
   */
  public void clearTables() {
    timetables_.clear();
  }

  public void clearTable(CalendarService service) {
    timetables_.remove(service.getID());
  }

  public void writeTableToFile(CalendarService service, String dpPath) { //, String path, String filename) {
    Timetable ttable = this.getTable(service);
    if (ttable == null) {
      System.out.println("err in writeTableToFile(): table does not exist");
    }

    String provID = containingRoute_.getProvider().getID();
    String subid = id_.toLowerCase().replaceAll("_", "");
    String fs = File.separator;
    String path = dpPath + "route" + fs + provID + containingRoute_.getID() + fs;
    String csvFile = provID + containingRoute_.getID() + "_" + subid + "_" + service.getID() + ".csv";

    //System.out.println("Writing: " + path + filename);
    try {
      new File(path).mkdirs();
      File file = new File(path+csvFile);
      file.createNewFile();
      
      CSVWriter writer = new CSVWriter(new FileWriter(file));
 
      String[] tptNames = new String[getPath().getTimePointCount()];
      int i=0;
      for (Iterator<TimePoint> it =  getPath().getTimePoints(); it.hasNext();)
        tptNames[i++] = it.next().getName();
      writer.writeNext(tptNames);
      
      int[][] table = ttable.getTable();
      
      for (int r = 0; r < table.length; r++) {
        String rowData[] = new String[path_.getTimePointCount()];
        for (int t = 0; t < path_.getTimePointCount(); t++) {
          rowData[t] = FPUtil.sTimeToStr(table[r][tptIndices_[t]], true);
        }
        writer.writeNext(rowData);
      }
      
      writer.close();
      ttable.setFileSaved(true);

      //getTable(day).setFilename(csvFile);

      
    } catch (Exception ex) {
      System.out.println("error writing table to csv file");
      ex.printStackTrace();
      //Logger.getLogger(SubRoute.class.getName()).log(Level.SEVERE, null, ex);
    }
  }

  private int[][] getTableArr(CalendarService service) {
    return getTable(service).getTable();
  }

  public int getRunCount(CalendarService service) {
    return getTableArr(service).length;
  }

  public int stopCount() {
    return path_.getAllStopCount();
  }

  public void addLinkID(Integer linkID) {
    linkIDs_.add(linkID);
  }

  public Collection<Integer> getLinkIDs() {
    if(path_.linkCount() > 0) {
      List<Integer> ids = new LinkedList<Integer>();
      for(BLink link : path_.getLinkList())
        ids.add(link.getID());
      return ids;
    }
    return linkIDs_;
  }

  public int linkCount() {
    if(path_.linkCount() > 0) return path_.linkCount();
    return linkIDs_.size();
  }

  public void initPath(BaseNetwork net) {
    //System.out.println("initPath "+this.getMasterID());
    for(Integer id : linkIDs_) {
      try {
        path_.initLink(net.getLinkFromID(id));
      } catch (Exception e) {
        System.out.println("Error loading path for " + this.getRoute().getProvider().getID() + this.getFullID() + ", link id=" + id);
        path_.clear();
      }
    }
    for(Stop stop : path_.getStops()) {
      stop.initNode(net);
    }

    calculateBasicStopTimes();
  }

  public boolean initialized() {
    System.out.println(" initialized "+linkIDs_.size() + " "+path_.linkCount() );

    return path_.linkCount() > 0; // || linkIDs_.size() == 0 && path_.linkCount() > 0;
  }

  public void initTimetable(CalendarService service, int runs, boolean fileSaved) {
    timetables_.put(service.getID(), new Timetable(service, runs, path_.getAllStopCount(), fileSaved));
    initTimePointIndices();
  }

  public void initTimePointIndices() {
    tptIndices_ = new int[path_.getTimePointCount()];
    for (int i = 0; i < path_.getTimePointCount(); i++) {
      tptIndices_[i] = path_.getMasterIndex(i + 1);
    //System.out.printf(" tptI[%d]=%d\n",i,tptIndices_[i]);
    }
  }

  public int getTimePointStopIndex(int tptIndex) {
    return tptIndices_[tptIndex];
  }

  public void setRunTimePoint(CalendarService service, int run, int tptIndex, int time) {
    int stopIndex = tptIndices_[tptIndex];//path_.getMasterIndex(tptIndex);
    //System.out.println("["+run+","+stopIndex+"]="+time);
    if (stopIndex < 0) {
      return;
    }
    getTable(service).setTime(run, stopIndex, time);
  }

  public void setTimePointName(int index, String name) {
    path_.getTimePoint(index).setName(name);
  }

  public void calculateNewTimePointTimes(int tptIndex) {
    //System.out.println("calcNewTPtTimes tpti="+tptIndex);
    for(Timetable table : getTables()) {
      calculateNewTimePointTimes(table, tptIndex);
    }
  }

  private void calculateNewTimePointTimes(Timetable table, int tpt) {
    int prevIndex = tptIndices_[tpt - 2], thisIndex = tptIndices_[tpt - 1], nextIndex = tptIndices_[tpt];
    //System.out.printf("prevI=%d nextI=%d\n", prevIndex, nextIndex);
    double tptSpan = (double) path_.getDistanceBetweenStops(prevIndex, nextIndex, false);
    double subSpan = (double) path_.getDistanceBetweenStops(prevIndex, thisIndex, false);
    double split = subSpan / tptSpan;
    for (int r = 0; r < table.runCount(); r++) {
      double prevTime = (double) table.getTime(r, prevIndex);
      double nextTime = (double) table.getTime(r, nextIndex);
      double newTime = prevTime + (nextTime - prevTime) * split;
      table.setTime(r, thisIndex, (int) newTime);
    //System.out.printf("setTime i=%d time=%d\n", tptIndices_[tpt-1], (int) newTime);
    }
  }

  public void calculateBasicStopTimes() {
    for(Timetable table : getTables()) {
      calculateBasicStopTimes(table);
    }
  }

  public void calculateBasicStopTimes(CalendarService service) {
    calculateBasicStopTimes(getTable(service));
  }

  private void calculateBasicStopTimes(Timetable table) {
    int stopCount = path_.getAllStopCount();
    if (stopCount == 0 || table == null) {
      return;
    }
    for (int r = 0; r < table.runCount(); r++) {
      for (int tpt = 0; tpt < tptIndices_.length - 1; tpt++) {
        int tptIndex1 = tptIndices_[tpt], tptIndex2 = tptIndices_[tpt + 1];
        double sTime = (double) table.getTime(r, tptIndex1);
        double tTime = (double) table.getTime(r, tptIndex2);
        double tptSpan = (double) path_.getDistanceBetweenStops(tptIndex1, tptIndex2, false);
        for (int i = tptIndex1 + 1; i < tptIndex2; i++) {
          double subSpan = (double) path_.getDistanceBetweenStops(tptIndex1, i, false);
          double pos = subSpan / tptSpan;

          double dtime = sTime + pos * (tTime - sTime);
          table.setTime(r, i, (int) dtime);
        }
      }
    }
  }

  public TableModel getTableModel(CalendarService service, boolean includeAllStops) {
    int tptCount = path_.getTimePointCount();
    int stopCount = path_.getAllStopCount();
    Timetable table = getTable(service);
    if (stopCount == 0 || table == null) {
      return new DefaultTableModel();
    }
    int numCols = (includeAllStops ? stopCount : tptCount);
    String[] colNames = new String[numCols];
    for (int i = 0; i < numCols; i++) {
      colNames[i] = path_.getStop(includeAllStops ? i : tptIndices_[i]).getName();
    }
    String[][] times = new String[table.runCount()][numCols];
    for (int r = 0; r < table.runCount(); r++) {
      for (int c = 0; c < numCols; c++) {
        int s = includeAllStops ? c : tptIndices_[c];
        times[r][c] = FPUtil.sTimeToStr(table.getTime(r, s));
      }
    }
    return new DefaultTableModel(times, colNames);
  }

  public void addStopToTimetables(int stop) {
    addStopToTimetables(stop, true);
  }

  public void addStopToTimetables(int stop, boolean reinit) {
    for(Timetable table : getTables()) {
      addStop(table, stop, reinit);
    }
  }

  public void removeStopFromTimetables(int stop) {
    for(Timetable table : getTables()) {
      removeStopFromTimetable(table, stop);
    }
  }

  private void removeStopFromTimetable(Timetable table, int stop) {
    //System.out.println("removing stop "+stop+" for day "+table.getDay());
    if (table == null) {
      return;
    }
    int buffer[][] = table.getTable();
    int oldStopCount = table.stopCount();
    //System.out.println("old StopCount "+table.stopCount());
    table.init(table.runCount(), oldStopCount);
    //System.out.println("new StopCount "+table.stopCount());
    int r, s;
    for (r = 0; r < table.runCount(); r++) {
      for (s = 0; s < stop; s++) {
        table.setTime(r, s, buffer[r][s]);
      }
    }
    for (r = 0; r < table.runCount(); r++) {
      for (s = stop; s < oldStopCount - 1; s++) {
        table.setTime(r, s, buffer[r][s + 1]);
      }
    }
    initTimePointIndices();
  }

  private void addStop(Timetable table, int stop, boolean reinit) {
    //System.out.println("adding stop i: "+stop);
    if (table == null) {
      return;
    }
    int buffer[][] = table.getTable();
    //System.out.printf("table dim: %d by %d\n", buffer.length, buffer[0].length);
    int oldStopCount = stopCount() - 1;
    table.init(table.runCount(), oldStopCount + 1);//+1);
    int r, s;

    // copy columns before the new stop
    for (r = 0; r < table.runCount(); r++) {
      for (s = 0; s < stop; s++) {
        table.setTime(r, s, buffer[r][s]);
      }

    }

    // copy columns after stop, bumped over one column to make room for new stop
    for (r = 0; r < table.runCount(); r++) {
      for (s = stop; s < oldStopCount; s++) {
        //System.out.printf("setting time at %d, %d\n", r, s+1);
        table.setTime(r, s + 1, buffer[r][s]);
      }
    }

    // reinitialize tpt indices and stop times 
    if (reinit) {
      initTimePointIndices();
      if (path_.getStop(stop).isTimePoint()) {
        calculateNewTimePointTimes(((TimePoint) path_.getStop(stop)).getIndex());
      }
      calculateBasicStopTimes(table);
    }
  }

  public void initTransitElements(FPEngine engine) {
    if (path_.linkCount() == 0) {
      return;
    }


    // scan direct path
    path_.createDirectPath(engine.getBaseNet());
    Iterator<BLink> dlinks = path_.getDirectPath().getLinks();
    int dli = 0;
    while (dlinks.hasNext()) {
      BLink dlink = dlinks.next();
      //Edge e = dlink.getEdge();
      BNode sNode = path_.getStop(dli).getNode();
      BNode fNode = engine.getNodeFromID(new Integer(dlink.getFNodeID()));
      BNode tNode = engine.getNodeFromID(new Integer(dlink.getTNodeID()));
      if (sNode == fNode) {
        dlink.registerSubRouteFT(this, dli);
      } else if (sNode == tNode) {
        dlink.registerSubRouteTF(this, dli);
      } else {
        System.err.println("Error: stop node not coincident with either endnode");
      }

      // if a d-link, scan and register corresponding p-links
      BNode node = sNode;
      if (dlink.getClassType() == BLink.CLASS_DIRECT) {
        int pli1 = path_.getFirstPLIFromDLI(dli);
        int pli2 = path_.getLastPLIFromDLI(dli);
        //System.out.println("d-link: "+pli1+" to "+pli2);
        for (int pli = pli1; pli <= pli2; pli++) {
          BLink link = path_.getLink(pli);
          if (node == link.getFNode()) {
            //System.out.println(" rFT link "+link.getID());
            link.registerSubRouteFT(this, -1);
          } else if (node == link.getTNode()) {
            //System.out.println(" rTF link "+link.getID());
            link.registerSubRouteTF(this, -1);
          } else {
            System.err.println("Error: stop node not coincident with either endnode");
          }
          node = engine.getBaseNet().opposite(node, link);
        }
      }
      dli++;
    }

  /*
  // scan full path, register remaining 
  Iterator<CLink> links = path_.getLinks();
  CNode node = path_.startNode();
  while(links.hasNext()) {
  CLink link = links.next();
  if(node == link.getFNode()) link.registerSubRouteFT(this, -1);	
  else if(node == link.getTNode()) link.registerSubRouteTF(this, -1);
  else System.err.println("Error: stop node not coincident with either endnode!");
  node = engine.getBaseNet().opposite(node, link);
  }*/

  }

  public int compareTo(Object o) {
    SubRoute sub2 = (SubRoute) o;
    if(this.getRoute().getProvider() != sub2.getRoute().getProvider())
      return this.getRoute().getProvider().getName().compareTo(sub2.getRoute().getProvider().getName());
    if(!this.getRoute().getID().equals(sub2.getRoute().getID())) {
      if(FPUtil.isInteger(this.getRoute().getID()) && FPUtil.isInteger(sub2.getRoute().getID()))
        return new Integer(this.getRoute().getID()).compareTo(new Integer(sub2.getRoute().getID()));
      return this.getRoute().getID().compareTo(sub2.getRoute().getID());
    }
    return this.getID().compareTo(sub2.getID());

  }

  public class CTravelTimeInfo {

    private int travTime_,  boardTime_,  run_;

    public CTravelTimeInfo(int travTime, int boardTime, int run) {
      travTime_ = travTime;
      boardTime_ = boardTime;
      run_ = run;
    }

    public int getTravelTime() {
      return travTime_;
    }

    public void setTravelTime(int travTime) {
      travTime_ = travTime;
    }

    public int getBoardTime() {
      return boardTime_;
    }

    public void setBoardTime(int boardTime) {
      boardTime_ = boardTime;
    }

    public int getRun() {
      return run_;
    }

    public void setRun(int run) {
      run_ = run;
    }
  }

  public CTravelTimeInfo getTravelTime(CalendarService service, int arrTime, int dli) {
    //int time = Integer.MAX_VALUE;
    Timetable table = getTable(service);
    CTravelTimeInfo tti = new CTravelTimeInfo(Integer.MAX_VALUE, 0, 0);
    if (arrTime < 0 || table == null) {
      return tti;
    }
    int runCount = table.runCount();
    for (int r = 0; r < runCount; r++) {
      //System.out.println("arr "+arrTime+" rd "+table_[r][dli]);
      if (table.getTime(r, dli) >= arrTime) {
        int travelTime = table.getTime(r, dli + 1) - arrTime; //table_[r][dli];
        if (travelTime > 0 && travelTime < tti.getTravelTime()) {
          tti.setTravelTime(travelTime);
          tti.setBoardTime(table.getTime(r, dli));
          tti.setRun(r + 1);
        }
      }
    }
    //System.out.println("tr "+time);
    return tti;
  }

  public int getNextRun(CalendarService service, int arrTime, int dli) {
    return getNextRun(service, arrTime, dli, false);
  }

  public int getNextRun(CalendarService service, int arrTime, int dli, boolean dbg) {
    //if(this.getFullID().equals("NE-S:NB")) System.out.println("   arrtime="+arrTime);
    Timetable table = getTable(service);
    int time = Integer.MAX_VALUE;
    int run = 0;
    if (arrTime < 0 || table == null) {
      return run;
    }
    int runCount = table.runCount();
    for (int r = 0; r < runCount; r++) {
      if (dbg) {
        System.out.println("   " + table.getTime(r, dli));
      }
      if (table.getTime(r, dli) >= arrTime) {
        int travelTime = table.getTime(r, dli + 1) - arrTime;
        if (travelTime > 0 && travelTime < time) { // provision for unsorted run tables
          time = travelTime;
          run = r + 1;
          if (dbg) {
            System.out.println("   run=" + run);
          }
        }
      }
    }
    return run;
  }

  public int getPrevRun(CalendarService service, int depTime, int dli) {
    Timetable table = getTable(service);
    int time = Integer.MAX_VALUE;
    int run = 0;
    if (depTime < 0 || table == null) {
      return run;
    }
    int runCount = table.runCount();
    for (int r = 0; r < runCount; r++) {
      if (table.getTime(r, dli + 1) <= depTime) {
        int travelTime = depTime - table.getTime(r, dli);
        if (travelTime > 0 && travelTime < time) {
          time = travelTime;
          run = r + 1;
        }
      }
    }
    return run;
  }

  public int getTime(CalendarService service, int nonZeroBased, int stop) {
    try {
      return getTable(service).getTime(nonZeroBased - 1, stop);
    } catch (Exception ex) {
      System.out.println("Error getting time for " + this.getRoute().getCombinedID() + ":" + this.getID() + " run=" + nonZeroBased + " stop=" + stop);
      return 1000000;
    }
  }

  public int runsPerHour(CalendarService service, int h) { // h = hours after 4am
    if (!hasTable(service)) {
      return 0;
    }
    return getTable(service).runsPerHour(h);
  }

  public void setHeadway(CalendarService service, int fromHr, int toHr, int headway, int mph) {
    setHeadway(service, fromHr, toHr, headway, 0, mph);
  }

  public void setHeadway(CalendarService service, int fromHr, int toHr, int headway, int offset, int mph) {

    // determine how many runs are to be retained from the existing table, if applicable
    int runCount = 0;
    Timetable oldTable = null;
    if (hasTable(service)) {
      oldTable = getTable(service);
      for (int r = 0; r < oldTable.getTable().length; r++) {
        int hr = oldTable.getTable()[r][0] / 3600;
        if (hr < fromHr || hr >= toHr) {
          runCount++;
        }
      }
      //System.out.println("table exists; " + runCount + " of " + oldTable.getTable().length + " runs retained");
    } else {
      //System.out.println("table does not exist");
    }

    // determine the size of the new table and (re)initialize it
    int newRuns = (headway == 0) ? 0 : (((toHr - fromHr) * 60) - offset) / headway;// + 1; // headway == 0: code for no runs during specified hour(s)
    //System.out.println("newRuns=" + newRuns);
    runCount += newRuns;
    //System.out.println("reinitializing table w/ len=" + runCount);
    this.initTimetable(service, runCount, false);

    // populate the new table with the retained runs
    Timetable newTable = getTable(service);
    int r = 0;
    if (oldTable != null) {
      for (int or = 0; or < oldTable.getTable().length; or++) {
        int hr = oldTable.getTable()[or][0] / 3600;
        if (hr < fromHr || hr >= toHr) { // copy row
          for (int c = 0; c < oldTable.getTable()[r].length; c++) {
            newTable.getTable()[r][c] = oldTable.getTable()[or][c];
          }
          r++;
        }
      }
      //System.out.println("last r=" + r);
    }

    // if we're only clearing runs, we're done
    if(headway == 0) {
      calculateBasicStopTimes(newTable);
      return;
    }

    // populate the added run entries in the new table
    double pathLenFt = (double) path_.lengthFt(true);
    //System.out.println("pathLenFt="+pathLenFt);
    double pathLenMi = pathLenFt / 5280; // convert to miles
    double runLenSec = 3600.0 * pathLenMi / ((double) mph);
    //System.out.println("runLenSec = "+runLenSec);
    int secArr[] = new int[path_.getTimePointCount()];
    secArr[0] = 0;
    for (int tpi = 2; tpi < path_.getTimePointCount(); tpi++) {
      double distFt = path_.getDistanceBetweenStops(0, path_.getMasterIndex(tpi), true);
      //System.out.println("distFt=" + distFt);
      double segLenSec = runLenSec * (distFt / pathLenFt);
      secArr[tpi - 1] = (int) segLenSec;
    }
    secArr[path_.getTimePointCount() - 1] = (int) runLenSec;

    for (int nr = 0; nr < newRuns; nr++) {
      int startTime = (3600 * fromHr) + (offset * 60) + (nr *  headway * 60);
      for (int c = 0; c < secArr.length; c++) {
        newTable.getTable()[r][path_.getMasterIndex(c + 1)] = startTime + secArr[c];
      }
      r++;
    }
    this.calculateBasicStopTimes(newTable);
  }

  public void addRouteLink(RLinkSub rLink) {
    routeLinks_.add(rLink);
  }

  public void clearRouteLinks(RouteNetwork container) {
    Iterator<RLinkSub> links = routeLinks_.iterator();
    while (links.hasNext()) {
      container.removeLink(links.next());
    }
    routeLinks_.clear();
  }

  public RLinkSub getRouteLinkFromDLI(int dli) {
    Iterator<RLinkSub> rLinks = routeLinks_.iterator();
    while (rLinks.hasNext()) {
      RLinkSub rLink = rLinks.next();
      if (dli >= rLink.getStartDLI() && dli <= rLink.getEndDLI()) {
        return rLink;
      }
    }
    return null;
  }

  public int getRouteLinkIndex(int dli) {
    Iterator<RLinkSub> rLinks = routeLinks_.iterator();
    int i = 0;
    while (rLinks.hasNext()) {
      RLinkSub rLink = rLinks.next();
      if (dli >= rLink.getStartDLI() && dli <= rLink.getEndDLI()) {
        return i;
      }
      i++;
    }
    return -1;
  }

  public RLinkSub getRouteLink(int i) {
    return routeLinks_.get(i);
  }

  public int routeLinkCount() {
    return routeLinks_.size();
  }

  /*public void drawAnimation(CalendarService service, int run, int time, MapCanvas canvas) {

      Point2D.Double pt = getLocationAtTime(service, run, time)[0];
      Color c = path_.getType().getColor(); //Color.black;
      int r = 0;
      switch (path_.getType()) {
        case BUS: r = 6; break;
        case METRO: r = 12; break;
        case REGIONALRAIL: r = 12; break;
        case LIGHTRAIL: r = 10; break;
      }
      canvas.setColor(Color.white);
      canvas.drawPoint(pt.x, pt.y, r + 4);
      canvas.setColor(c);
      canvas.drawPoint(pt.x, pt.y, r);

    
  }*/

//  public Point2D.Double[] getLocationAtTime(CalendarService service, int run, int stime) {
//
//    int startTime = getTime(service, run, 0), endTime = getTime(service, run, stopCount()-1);
//    // return null if query time is not within this run's timespan
//    if(stime < startTime || stime > endTime) {
//      System.out.println("WARNING: getLocationAtTime(): time "+FPUtil.sTimeToStr(stime)+" outside of range "+FPUtil.sTimeToStr(startTime)+" to "+FPUtil.sTimeToStr(endTime));
//      return null;
//    }
//
//    // special case if query time is at very start
//    if(stime == startTime) return path_.getPoint(0);
//
//    // typically, find which two timepoints the query time is between
//    int tptIndex;
//    for(tptIndex = 0; tptIndex < tptIndices_.length-1; tptIndex++) {
//      if(getTime(service, run, tptIndices_[tptIndex]) < stime &&
//              stime <= getTime(service, run, tptIndices_[tptIndex+1]))
//        break;
//    }
//
//    Path subpath = path_.subPath(path_.getStop(tptIndices_[tptIndex]).getOutPLI(),
//            path_.getStop(tptIndices_[tptIndex+1]).getInPLI());
//
//    double timeSpan = getTime(service, run, tptIndices_[tptIndex+1]) - getTime(service, run, tptIndices_[tptIndex]);
//    double timePastTPt = stime - getTime(service, run, tptIndices_[tptIndex]);
//
//    double i = timePastTPt/timeSpan;
//    //System.out.println("i="+i);
//    return subpath.getPoint(i);
//  }

//    public double getLocationAtTime(CalendarService service, int run, int stime) {
//
//    int startTime = getTime(service, run, 0), endTime = getTime(service, run, stopCount()-1);
//
//    // return -1 if query time is not within this run's timespan
//    if(stime < startTime || stime > endTime) {
//      //System.out.println("WARNING: getLocationAtTime(): time "+FPUtil.sTimeToStr(stime)+" outside of range "+FPUtil.sTimeToStr(startTime)+" to "+FPUtil.sTimeToStr(endTime));
//      return -1;
//    }
//
//    // special case if query time is at very start
//    if(stime == startTime) return 0;
//
//    // typically, find which two timepoints the query time is between
//    double r = 0; // the value between 0 and 1 to return
//    double totalLength = path_.lengthFt(false);
//
//    int tptIndex;
//    Path subpath = null;
//    for(tptIndex = 0; tptIndex < tptIndices_.length-1; tptIndex++) {
//      subpath = path_.subPath(path_.getStop(tptIndices_[tptIndex]).getOutPLI(),
//                              path_.getStop(tptIndices_[tptIndex+1]).getInPLI());
//      if(getTime(service, run, tptIndices_[tptIndex]) < stime &&
//              stime <= getTime(service, run, tptIndices_[tptIndex+1]))
//        break;
//      else
//        r += ((double)subpath.lengthFt(false)) / totalLength;
//    }
//
//    double timeSpan = getTime(service, run, tptIndices_[tptIndex+1]) - getTime(service, run, tptIndices_[tptIndex]);
//    double timePastTPt = stime - getTime(service, run, tptIndices_[tptIndex]);
//
//    double i = timePastTPt/timeSpan;
//    r += (i * (double) subpath.lengthFt(false)) / totalLength;
//    return r;
//  }


  public SerializableSubRoute getSerializable() {
    List<Integer> linkIDs = new LinkedList<Integer>();
    Iterator<BLink> links = path_.getLinks();
    while (links.hasNext()) {
      linkIDs.add(links.next().getID());
    }

    List<SerializableStop> stops = new LinkedList<SerializableStop>();
    Iterator<Stop> stopIter = path_.getStopIter();
    while (stopIter.hasNext()) {
      stops.add(stopIter.next().getSerializable());
    }

    return new SerializableSubRoute(id_, name_, rteSuffix_, path_.getType(), linkIDs, stops, timetables_);
  }
}



















